Skip to content

Add icons to model types - #6865

Merged
lkostrowski merged 7 commits into
mainfrom
lkostrowski/model-type-icons
Sep 2, 2026
Merged

Add icons to model types#6865
lkostrowski merged 7 commits into
mainfrom
lkostrowski/model-type-icons

Conversation

@lkostrowski

@lkostrowski lkostrowski commented Aug 24, 2026

Copy link
Copy Markdown
Member

Model types are told apart by name alone today, which gets slow to scan once a shop has more than a handful. This gives each one an optional Lucide icon and a colour, stored in public metadata on PageType (dashboard-icon-name, dashboard-icon-color), and renders it everywhere a model type is listed or referenced.

Screenshots

Screenshot Where this is
Icon picker, nothing selected Modeling → Model Types → open a type. The picker sits in the right-hand General Information card, immediately left of the Name field. It is an ordinary form field: the choice is held in form state and written by the page's Savebar, not saved on click.
Icon picker with a search query Same card, popover open. The grid is the full Lucide set, loaded lazily the first time the popover is opened rather than shipped with the page.
All model types list Modeling → Model Types. The list behind that section's settings sub-item — the index that the edit page above is reached from.
Sidebar Favorites section Main sidebar. These rows exist because a model type was pinned from the models list via Pin to nav. Favorites is one of several sections a pin can target; the same treatment applies wherever else a pin lands.
Model list type tabs Modeling (the models list itself). The tab strip above the search field, generated from the shop's model types, with adjacent types optionally collapsed into a grouped tab.
Model list Model Type column Same models list, the Model Type column of the datagrid below those tabs. This surface is glide-data-grid, so it paints on <canvas> rather than rendering React.
Model reference chip in the Attributes card Modeling → open a model → Attributes card. The chip comes from an attribute whose input type is Reference and whose entity type is Model. The same card and the same chip appear on products carrying a model reference.

What it does

  • Set an icon + colour on a model type's edit page. Six colours; light/dark pairs resolved from one table.
  • Clear it with Reset, which deletes both metadata keys rather than blanking them, so the metadata editor stays free of dead entries.
  • Fallback — model types without an icon render a neutral Shapes glyph everywhere. The slot is a fixed width, so configuring one doesn't reflow the list around it.
  • Renders in 8 places — model type list, model type detail TopNav title, sidebar pins, the pin manager and pin dialog, model list tabs, the model list "Model Type" column, the model type picker dialog, and model reference chips.
  • Search matches icon names only, and results are capped at 60 with a "keep typing to narrow down" hint — every rendered tile fires its own lazy import, so an uncapped grid over ~1900 icons would fire that many requests.
  • Degrades quietly — a renamed or hand-edited icon name falls back to the placeholder instead of breaking the view it sits in. A colour stored without a name is inert.

Notes for review

PageTypeUpdateInput carries no metadata, so the icon rides a second call on submit. createMetadataCreateHandler couldn't be reused: it hardcodes keysToDelete: [], which would have left Reset a silent no-op. UpdateMetadata carries both halves in one round trip and merges by key, so clearing works and unrelated metadata is untouched.

There is a build-config change in vite.config.js and it is load-bearing. Icons resolve through dynamicIconImports, which keeps all ~1900 icon modules alive. The blanket node_modules → "vendor" rule would then inline every one of them into the main bundle. Measured against origin/main:

vendor (minified) vendor (gzip) chunks
origin/main 5,585,812 1,491,432 262
without the rule 6,343,318 252
this PR 5,703,552 1,511,773 1,903

So the naive version costs +757kB minified on every page load. An explicit chunk per icon keeps them out: vendor grows 20kB gzipped (+1.4%) and a rendered icon costs ~700 bytes, fetched once and cached. The trade-offs are 1,651 extra files in dist, and the ~34 icons the entry chunk uses statically becoming small separate requests instead of living inside vendor.

The datagrid can't await. Canvas draws synchronously, so it paints the fallback glyph and repaints once the visible page's icons resolve, rather than blocking the list on a set of lazy imports.

ChipField / SortableChip gained a startAdornment prop because their label intersects with an HTML attribute on BoxProps, typing it ReactNode & string — an element can't be passed through it.

  • Organize colors
  • Fill missing places where icon is missing

Model types can carry a Lucide icon and a colour, so they are recognisable at a
glance instead of being told apart by name alone. Both live in public metadata on
PageType, under `dashboard-icon-name` and `dashboard-icon-color`.

PageTypeUpdateInput carries no metadata, so the icon rides a second call on submit.
`createMetadataCreateHandler` could not be reused: it hardcodes `keysToDelete: []`,
which would leave "Reset" a silent no-op. `UpdateMetadata` carries both halves in one
round trip and merges by key, so clearing works and unrelated metadata is untouched.

Icons resolve through `dynamicIconImports`, which keeps all ~1900 icon modules alive.
The blanket `node_modules -> "vendor"` chunk rule would then inline every one of them
into the main bundle, measured at +757kB minified against origin/main. An explicit
chunk per icon keeps them out: vendor grows 20kB gzipped and a rendered icon costs
about 700 bytes. The cost is 1651 extra files in dist.

The datagrid paints on canvas and cannot await an import, so it draws the fallback
glyph and repaints once the visible page's icons land, rather than blocking the list.
Lucide ships each icon's raw node data next to the component, so the existing
`iconNodeToSvg` and `drawIconLabelCell` cover the cell.

Types without an icon render a neutral `Shapes` fallback, which is imported statically
and doubles as the placeholder for the frame before a name resolves — the icon slot
never renders empty and never reflows.

Picker results are capped at 60 rather than virtualised, since every rendered tile
fires its own lazy import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 24, 2026 11:09
@changeset-bot

changeset-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e3834c1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
saleor-dashboard Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@lkostrowski lkostrowski added the test deployment Deploy Pull Request to *.saleor.rocks environment label Aug 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces configurable model type (PageType) icons + colors stored in public metadata (dashboard-icon-name, dashboard-icon-color) and threads them through the Dashboard UI so model types are visually recognizable across lists, tabs, pins, pickers, and reference chips. It also adjusts bundling so Lucide’s dynamically imported icons don’t bloat the main vendor bundle.

Changes:

  • Add a ModelTypeIcon system (constants, metadata read/write helpers, lazy Lucide loading, icon picker UI, canvas/datagrid rendering).
  • Extend relevant GraphQL selections/fragments/fixtures to include PageType.metadata where needed for icon resolution.
  • Render the chosen icon in key UX surfaces (model type list/details, model list tabs + datagrid “Type” column, navigation pins + manager, model reference chips).

Reviewed changes

Copilot reviewed 60 out of 60 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vite.config.js Split Lucide icon modules into per-icon chunks to keep them out of the main vendor bundle.
src/searches/usePageTypeSearch.ts Include metadata in page type search results for icon resolution.
src/searches/usePageSearch.ts Include pageType.metadata in page search results for icon resolution.
src/navigationPins/types.ts Add icon to resolved navigation pin type.
src/navigationPins/injectNavigationPins.tsx Render pin icon in sidebar menu items.
src/navigationPins/injectNavigationPins.test.ts Update pin test factory to include icon.
src/navigationPins/hooks/useResolvedNavigationPins.ts Resolve icons for pins with fallback.
src/navigationPins/hooks/usePinnedModelTypeNames.ts Return { names, icons } lookup from pinned model types.
src/navigationPins/fragments/pinnedModelType.ts Add metadata { key value } to pinned model type fragment.
src/navigationPins/components/PinModelTypeDialog.tsx Show model type icon in the pin dialog (when available).
src/navigationPins/components/NavigationPinList.tsx Show icons in the pin manager list.
src/navigationPins/components/NavigationPinButton.tsx Pass selected model type icon into the pin flow.
src/navigationPins/buildPinnedModelTypesDocument.test.ts Snapshot update for new fragment fields.
src/modelTypes/views/PageTypeDetails.tsx Persist icon via metadata update after page type update.
src/modelTypes/utils/pageTypePageForm.ts Treat icon changes as making the form “dirty”.
src/modelTypes/utils/pageTypePageForm.test.ts Update test data for new icon field.
src/modelTypes/fixtures.ts Add metadata: [] in page type fixtures.
src/modelTypes/components/PageTypeList/PageTypeList.tsx Render model type icon in the page type list rows.
src/modelTypes/components/PageTypeDetailsPage/Title.tsx Render model type icon in the details title/header.
src/modelTypes/components/PageTypeDetailsPage/PageTypeDetailsPage.tsx Add icon to form state and wire icon changes to Form.set.
src/modelTypes/components/PageTypeDetails/PageTypeDetails.tsx Add ModelTypeIconPicker next to the name field.
src/modeling/views/PageList/PageList.tsx Use icon-bearing options for the page type picker dialog.
src/modeling/queries.ts Include metadata in pageTypeQuery selection.
src/modeling/fixtures.ts Add metadata: [] in modeling fixtures.
src/modeling/components/PageListPage/PageListPage.tsx Compute active type icon and pass to NavigationPinButton.
src/modeling/components/PageListDatagrid/PageListDatagrid.tsx Preload Lucide icons and repaint datagrid when cached.
src/modeling/components/PageListDatagrid/datagrid.ts Render model type cell with icon in the “Type” column.
src/modeling/components/ModelTypeTabs/ModelTypeTabs.tsx Render icons in type/group tabs where applicable.
src/graphql/types.generated.ts Regenerate types to reflect new metadata selections.
src/graphql/hooks.generated.ts Regenerate hooks/documents to include MetadataItem usage.
src/fragments/pageTypes.ts Add metadata { ...MetadataItem } to PageType fragment.
src/fragments/pages.ts Add pageType.metadata to Page fragment.
src/components/SortableChipsField/SortableChipsField.tsx Carry per-chip model type icon for sortable reference chips.
src/components/SortableChip/SortableChip.tsx Add startAdornment slot to sortable chips.
src/components/ModelTypeIcon/usePreloadedLucideIcons.ts Hook to preload icons and trigger repaint for canvas consumers.
src/components/ModelTypeIcon/toModelTypeOption.tsx Map model type nodes to combobox options with icon adornment.
src/components/ModelTypeIcon/renderModelTypeIconSvg.ts Render model type icon SVG for canvas usage.
src/components/ModelTypeIcon/ModelTypeIconPicker.tsx UI for searching/selecting icon + color (and resetting).
src/components/ModelTypeIcon/ModelTypeIconPicker.stories.tsx Storybook coverage for icon picker.
src/components/ModelTypeIcon/ModelTypeIconPicker.module.css Styles for the icon picker grid/swatches/trigger.
src/components/ModelTypeIcon/ModelTypeIcon.tsx Lazy-load Lucide icon component with stable fallback glyph.
src/components/ModelTypeIcon/ModelTypeIcon.stories.tsx Storybook coverage for icon rendering and edge cases.
src/components/ModelTypeIcon/messages.ts i18n messages for icon picker UI.
src/components/ModelTypeIcon/loadLucideIcon.ts Lazy icon import map + caching + preload helpers.
src/components/ModelTypeIcon/getModelTypeIcon.ts Read/write icon metadata helpers + fallback + equality helper.
src/components/ModelTypeIcon/getModelTypeIcon.test.ts Unit tests for metadata read/write/equality behavior.
src/components/ModelTypeIcon/constants.ts Define metadata keys, icon colors, and color resolution.
src/components/Datagrid/customCells/useCustomCellRenderers.ts Register model type custom cell renderer.
src/components/Datagrid/customCells/ModelTypeCell.tsx Canvas cell implementation for icon+label model type cells.
src/components/ChipField/ModelTypeChipIcon.tsx Render model type icon in reference chips when present.
src/components/ChipField/ChipField.tsx Add startAdornment slot to chips.
src/components/Attributes/utils.ts Thread model type icon through reference display value helpers.
src/components/Attributes/SingleReferenceField.tsx Show model type icon in selected single-reference chip.
src/components/AssignModelDialog/AssignModelDialog.test.tsx Update mocks to include pageType.metadata.
src/attributes/utils/getAssignedModelTypesForAttribute.test.ts Update fixtures with metadata: [].
src/attributes/utils/data.ts Attach model type icon to page references.
src/attributes/utils/data.test.ts Assert reference display includes fallback icon.
src/attributes/utils/computeTypeTabCounts.test.ts Update fixtures with metadata: [].
locale/defaultMessages.json Add localized strings for the icon picker UI.
.changeset/model-type-icons.md Changeset describing the feature and metadata storage keys.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +21 to +26
export const ModelTypeIcon = ({
icon,
size = iconSize.small,
strokeWidth = iconStrokeWidthBySize.small,
className,
}: ModelTypeIconProps) => {
…e-icons

# Conflicts:
#	src/graphql/hooks.generated.ts
#	src/modeling/components/PageListPage/PageListPage.tsx
Copilot AI review requested due to automatic review settings August 24, 2026 11:25
@lkostrowski lkostrowski added test deployment Deploy Pull Request to *.saleor.rocks environment and removed test deployment Deploy Pull Request to *.saleor.rocks environment labels Aug 24, 2026
@github-actions
github-actions Bot temporarily deployed to pr-6865 August 24, 2026 11:30 Destroyed
The picker was switched to the shared `buttonMessages.reset` after messages were
last extracted, leaving its own "Reset" entry orphaned in defaultMessages.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.32468% with 76 lines in your changes missing coverage. Please review.
✅ Project coverage is 58.17%. Comparing base (7e93191) to head (e3834c1).

Files with missing lines Patch % Lines
...c/components/ModelTypeIcon/ModelTypeIconPicker.tsx 67.85% 17 Missing and 1 partial ⚠️
...rc/navigationPins/hooks/usePinnedModelTypeNames.ts 0.00% 12 Missing and 2 partials ⚠️
src/modelTypes/views/PageTypeDetails.tsx 0.00% 7 Missing and 2 partials ⚠️
...components/ModelTypeIcon/ModelTypeIcon.stories.tsx 0.00% 8 Missing ⚠️
...ents/ModelTypeIcon/ModelTypeIconPicker.stories.tsx 0.00% 7 Missing ⚠️
.../modeling/components/PageListPage/PageListPage.tsx 0.00% 4 Missing ⚠️
src/components/ModelTypeIcon/loadLucideIcon.ts 91.42% 3 Missing ⚠️
...onents/PageTypeDetailsPage/PageTypeDetailsPage.tsx 40.00% 3 Missing ⚠️
.../navigationPins/components/NavigationPinButton.tsx 0.00% 2 Missing ⚠️
.../navigationPins/hooks/useNavigationPinListItems.ts 0.00% 2 Missing ⚠️
... and 5 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6865      +/-   ##
==========================================
+ Coverage   58.09%   58.17%   +0.07%     
==========================================
  Files        3374     3388      +14     
  Lines       73157    73440     +283     
  Branches    19337    19392      +55     
==========================================
+ Hits        42500    42722     +222     
- Misses      28785    28842      +57     
- Partials     1872     1876       +4     
Flag Coverage Δ
storybook 54.24% <83.33%> (+0.20%) ⬆️
units 51.71% <60.00%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 60 out of 60 changed files in this pull request and generated 2 comments.

Comment on lines +363 to +370
const iconsById = useMemo(
() =>
(pageTypes ?? []).reduce<Record<string, ModelTypeIconValue>>(
(acc, pageType) => ({ ...acc, [pageType.id]: getModelTypeIcon(pageType.metadata) }),
{},
),
[pageTypes],
);
Comment on lines +37 to +48
export const loadLucideIcon = (name: string): Promise<LoadedLucideIcon | null> => {
const cached = cache.get(name);

if (cached) {
return Promise.resolve(cached);
}

const inFlight = pending.get(name);

if (inFlight) {
return inFlight;
}
@lkostrowski lkostrowski added test deployment Deploy Pull Request to *.saleor.rocks environment and removed test deployment Deploy Pull Request to *.saleor.rocks environment labels Aug 24, 2026
…e-icons

# Conflicts:
#	src/attributes/utils/data.ts
#	src/modelTypes/components/PageTypeList/PageTypeList.tsx
#	src/modelTypes/views/PageTypeDetails.tsx
#	src/modeling/components/ModelTypeTabs/ModelTypeTabs.tsx
#	src/modeling/components/PageListDatagrid/PageListDatagrid.tsx
#	src/modeling/views/PageList/PageList.tsx
#	src/navigationPins/components/NavigationPinButton.tsx
#	src/navigationPins/components/NavigationPinList.tsx
#	src/navigationPins/hooks/usePinnedModelTypeNames.ts
#	src/navigationPins/hooks/useResolvedNavigationPins.ts
@lkostrowski
lkostrowski force-pushed the lkostrowski/model-type-icons branch from c84b521 to 9eec53d Compare September 2, 2026 11:27
lkostrowski and others added 3 commits September 2, 2026 14:03
…r trigger

Reference rows in the assign dialog listed models by name alone, so the icon that
identifies a model type everywhere else stopped at the dialog boundary. `SearchPages`
already selects the type's metadata, so no extra fetch is needed.

The adornment is a render prop on the dialog rather than a field on `Container`:
that shape doubles as the submit payload, and putting React nodes into submitted
data would leak presentation into every assign handler. Rows resolve the icon per
row instead.

The picker trigger is now an explicit square. `aspect-ratio` cannot do this here —
as a flex item its width resolves from content before `align-self: stretch` makes
the height definite, so the ratio had nothing to compute from and it rendered 34x52
against the input's 52.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reference chips resolved their label through one of four branches, and only
the reference-search branch carried the model type icon. Searches stay empty
until the assign dialog runs, so a saved reference always fell through to an
icon-less branch and rendered bare.

Look the icons up by id instead, so they no longer depend on which label
branch won: useModelReferenceIcons reuses the existing SearchPages query
(which already selects pageType.metadata) filtered to the assigned ids, and
the resulting map takes precedence in both reference display utils.

Also spaces the chip's adornment: SortableChip now uses a single gap on its
row rather than per-child margins, so the icon gets the same 4px as the drag
handle and an absent icon adds no stray gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lkostrowski
lkostrowski marked this pull request as ready for review September 2, 2026 12:26
@lkostrowski
lkostrowski requested review from a team, mirekm, peelar and stmpn and removed request for stmpn September 2, 2026 12:26
@lkostrowski
lkostrowski enabled auto-merge (squash) September 2, 2026 12:26
@lkostrowski
lkostrowski merged commit 232aa70 into main Sep 2, 2026
14 of 16 checks passed
@lkostrowski
lkostrowski deleted the lkostrowski/model-type-icons branch September 2, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test deployment Deploy Pull Request to *.saleor.rocks environment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants